2.9.6. 编写和使用自己的C库

程序员通常将大型 C 程序划分为相关功能的单独模块(即单独的.c文件)。多个模块共享的定义被放入头文件(.h文件)中,这些文件由需要它们的模块包含。同样,C 库代码也在一个或多个模块(.c文件)和一个或多个头文件(.h文件)中实现。 C 程序员经常实现自己的常用功能 C 库。通过编写库,程序员可以在库中实现该功能一次,然后可以在他们编写的任何后续 C 程序中使用该功能。

编译, 链接和C库使用部分中,我们介绍了如何使用、编译 C 库代码并将其链接到 C 程序中。在本节中,我们讨论如何用 C 语言编写和使用您自己的库。我们在这里介绍的内容也适用于构造和编译由多个 C 源文件和头文件组成的大型 C 程序。

要在 C 中创建库:

  1. 在头文件 (.h) 中定义库的接口。任何想要使用该库的程序都必须包含该头文件。
  2. 在一个或多个.c文件中创建该库的实现。这组函数定义实现了库的功能。有些函数可能是库的用户将调用的接口函数,而其他函数可能是库的用户无法调用的内部函数(内部函数是库实现的良好模块化设计的一部分)。
  3. 编译该库的二进制形式,该库可以链接到使用该库的程序中。

库的二进制形式可以直接从其源文件构建,作为编译使用该库的应用程序代码的一部分。此方法将库文件编译为.o文件并将它们静态链接到二进制可执行文件中。以这种方式包含库通常适用于您为自己使用而编写的库代码(因为您可以访问其.c源文件),并且它也是从多个.c模块构建可执行文件的方法。

或者,可以将库编译为二进制存档 (.a) 或共享对象 (.so) 文件,以供想要使用该库的程序使用。在这些情况下,库的用户通常无法访问库的 C 源代码文件,因此他们无法直接使用使用库源码来编译应用代码。当程序使用此类预编译库(例如.a.so)时,必须使用gcc-l命令行选项将库的代码显式链接到可执行文件中。

我们将详细讨论编写、编译和链接库代码的情况,其中程序员可以访问各个库模块(.c.o文件)。这一重点也适用于设计和编译分为多个.c.h文件的大型 C 程序。我们简要展示了用于构建归档库(静态库)和共享对象(动态共享库)的命令。有关构建这些类型的库文件的更多信息,请参阅gcc文档,包括gccar的手册页。

库详细信息示例(Library Details by Example)

下面,我们将展示一些创建和使用您自己的库的示例。

定义库接口:

头文件(.h 文件)是包含 C 函数原型和其他定义的文本文件——它们代表库的接口。任何想要使用该库的应用程序中都必须包含头文件。例如,C标准库头文件通常存储在/usr/include/中,可以使用编辑器查看:

$ vi /usr/include/stdio.h

下面是来自库的示例头文件 (mylib.h),其中包含库用户的一些定义。

#ifndef _MYLIB_H_
#define _MYLIB_H_

// a constant definition exported by library:
#define MAX_FOO  20

// a type definition exported by library:
struct foo_struct {
    int x;
    float y;
};

// a global variable exported by library
// "extern" means that this is not a variable declaration,
// but it defines that a variable named total_times of type
// int exists in the library implementation and is available
// for use by programs using the library.
// It is unusual for a library to export global variables
// to its users, but if it does, it is important that
// extern appears in the definition in the .h file
extern int total_times;

// a function prototype for a function exported by library:
// extern means that this function definition exists
// somewhere else.
/*
 * This function returns the larger of two float values
 *  y, z: the two values
 *  returns the value of the larger one
 */
extern float bigger(float y, float z);

#endif

头文件通常在其内容周围有特殊的“样板”代码:

#ifndef

// header file contents

#endif

此样板代码可确保编译器的预处理器仅在包含mylib.h的任何 C 文件中包含该内容一次。仅包含一次.h文件内容很重要,可以避免编译时出现重复定义错误(duplicate definition errors)。同样,如果您忘记在使用该库的 C 程序中包含.h文件,编译器将生成未定义符号(undefined symbol)警告。

.h 文件中的注释是库接口的一部分,是为库用户编写的。这些注释应该很详细,解释定义并描述每个库函数的作用、它采用的参数值以及它返回的内容。有时.h文件还会包含描述如何使用该库的顶级注释。

全局变量定义和函数原型之前的关键字extern意味着这些名称是在其他地方定义的。在库导出的任何全局变量之前包含extern尤其重要,因为它将名称和类型定义(在.h文件中)与库实现中的变量声明区分开来。在前面的示例中,全局变量在库内仅声明一次,但它通过库的.h文件中的extern定义导出给库用户。

实现库功能:

程序员在一个或多个.c文件(有时是内部.h文件)中实现库。该实现包括.h文件中所有函数原型的定义以及其实现内部的其他函数。这些内部函数通常使用关键字static定义,这将它们的可见性限制在定义它们的模块(.c文件)内。库实现还应包括.h文件中任何extern全局变量声明的变量定义。这是示例库实现 (mylib.c)

#include <stdlib.h>

// Include the library header file if the implementation needs
// any of its definitions (types or constants, for example.)
// Use " " instead of < > if the mylib.h file is not in a
// default  library path with other standard library header
// files (the usual case for library code you write and use.)
#include "mylib.h"

// declare the global variable exported by the library
int total_times = 0;

// include function definitions for each library function:
float bigger(float y, float z) {
    total_times++;
    if (y > z) {
        return y;
    }
    return z;
}

创建库的二进制形式:

要创建库的二进制形式(.o文件),请使用以下 -c选项进行编译:

$ gcc -o mylib.o -c mylib.c

一个或多个.o文件可以构建库的归档 ( .a) 或共享对象 ( .so) 版本。

  • 要构建静态库,请使用归档器 (ar):

    ar -rcs libmylib.a mylib.o
    
  • 要构建动态链接库,mylib.o目标文件必须使用位置无关代码(使用-fPIC)构建。 通过将gcc的标志指定为-shared,可以从mylib.o创建libmylib.so共享对象文件:

    gcc -fPIC -o mylib.o -c mylib.c
    gcc -shared -o libmylib.so mylib.o
    
  • 例如,共享对象和归档库通常是从多个.o文件构建的(请记住,动态链接库的.o需要使用-fPIC标志构建):

    gcc -shared -o libbiglib.so file1.o file2.o file3.o file4.o
    ar -rcs libbiglib.a file1.o file2.o file3.o file4.o
    

使用并链接库:

.c在使用该库的其他文件中:

  1. #include它的头文件
  2. 在编译期间显式链接到实现(.o文件)中。

包含库头文件后,您的代码就可以调用库的函数(例如,在 中myprog.c):

#include <stdio.h>
#include "mylib.h"   // include library header file

int main(void) {
    float val1, val2, ret;
    printf("Enter two float values: ");
    scanf("%f%f", &val1, &val2);
    ret = bigger(val1, val2);   // use a library function
    printf("%f is the biggest\n", ret);

    return 0;
}

`#include` 语法和预处理器

请注意,包含 mylib.h#include 语法与包含 stdio.h 的语法不同。这是因为 mylib.h 未与标准库中的头文件一起定位。预处理器有默认位置来查找标准头文件。当包含具有 <file.h> 语法而不是"file.h"语法的文件时,预处理器会在这些标准位置搜索头文件。

mylib.h 包含在双引号内时,预处理器首先在当前目录中查找 mylib.h 文件,然后通过指定 gcc 的包含路径 (-I) 来显式告诉它查找的其他位置。例如,如果头文件位于 /home/me/myincludes 目录中(并且与 myprog.c 文件不在同一目录中),则必须在 gcc 命令行中指定该目录的路径,以便预处理器找到 mylib.h 文件:

$ gcc -I/home/me/myincludes -c myprog.c

常见编译命令(从源码, 目标文件或库文件中构建执行程序)

  • 要将使用库 (mylib.o) 的程序 (myprog.c) 编译为二进制可执行文件:

    $ gcc -o myprog myprog.c mylib.o
    
  • 或者,如果库的实现文件在编译时可用,则可以直接从程序和库 .c 文件构建程序:

    $ gcc -o myprog myprog.c mylib.c
    
  • 或者,如果该库可作为归档或共享对象文件使用,则可以使用-l链接它,(-lmylib:请注意,库名称是libmylib.[a,so],但是仅mylib部分包含在gcc命令行中):

    $ gcc -o myprog myprog.c -L. -lmylib
    

    -L.选项指定libmylib.[so,a]文件的路径(-L后面的.表示应该搜索当前目录)。默认情况下,如果可以找到.so版本,gcc将动态链接库。有关链接和链接路径的详细信息,请参阅 2.9.5. 编译, 链接和C库使用

然后可以运行该程序:

$ ./myprog

如果您运行 的动态链接版本myprog,您可能会遇到如下错误:

/usr/bin/ld: cannot find -lmylib
collect2: error: ld returned 1 exit status

此错误表明运行时链接器在运行时找不到libmylib.so。要解决此问题,请设置LD_LIBRARY_PATH环境变量以包含libmylib.so文件的路径。 myprog的后续运行使用您添加到LD_LIBRARY_PATH的路径来查找libmylib.so文件并在运行时加载它。例如,如果libmylib.so位于/home/me/mylibs/子目录中,请在 bash shell 提示符下运行此命令(仅一次)以设置LD_LIBRARY_PATH环境变量:

$ export LD_LIBRARY_PATH=/home/me/mylibs:$LD_LIBRARY_PATH